
/*
    服务端流程
        1、创建套接字端口 
        2、为套接字绑定地址信息  网络通信数据包括 源端IP、源端端口、对端IP、对端端口、协议 此五元组
        3、开始监听
        4、获取一个新建立连接的socket描述符  获取socket的操作句柄，通过这个指定的socket操作句柄与指定的客户端通信
        5、收发数据  每个套接字都包含了完整的五元组，知道自己通信的地址，所以收发数据时不用设置地址
        6、关闭套接字  释放资源
    
    服务端接口
        创建套接字 int socket(int domain, int type, int protocol)
            domain 地址域 本地通信 AF_LOCAL, ipv4 AF_INET, ipv6 AD_INET6
            type 套接字类型 流式套接字 SOCK_STREAM, 数据报套接字 SOCK_DGRAM
            protocol 协议类型  tcp IPPROTO_TCP, UDP IPPROTO_UDP  
            失败返回-1，成功返回文件描述符-非负整数，套接字所有其他接口的操作句柄
        
        绑定地址信息 int bind(int sockfd, struct sockaddr *addr, socklen_t len)
            sockfd 创建套接字返回的操作句柄
            addr 要绑定的地址信息
            len 要绑定的地址信息长度 
        
        开始监听 listen(int sockfd, int backlog)
            sockfd 将套接字设置为监听状态，并且监听状态后可以开始接收客户端连接请求
            backlog 同一时间的并发连接数，决定同一时间最多接收多少个客户端的连接请求

        获取新连接 int accept(int sockfd, struct sockaddr * cli_addr, socklen_t * len)
            sockfd 获取那个tcp服务端套接字的新建连接
            cli_addr 新建套接字对应的客户端地址信息
            len 地址信息长度
            成功返回新建的socket套接字的描述符，即外部进程中对该套接字的操作句柄

        接收数据，接收发送者地址便于回复 ssize_t recv(int sockfd, char *buf, int len, int flag)
            sockfd 创建套接字返回的操作句柄
            buf 缓冲区，用于接收从接收缓冲区中取出的数据
            len 要接收的数据长度
            flag 操作选项标志，默认为0，表示阻塞操作
            成功返回实际接收到的数据字节长度，失败返回-1
        
        发送数据 ssize_t send(int sockfd, char *data, int len, int flag)
            sockfd 创建套接字返回的操作句柄
            data 要发送的数据地址
            len 要发送数据长度
            flag 默认为0，阻塞操作
            成功返回实际发送的数据字节长度，失败返回-1
        
        关闭套接字 int close(int sockfd)
*/


#include "tcpSocket.h"
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>


/*
多进程版本，注：
	父子进程数据独有，父进程用不到新建套接字，要记得关闭；进程等待使用信号处理，不再阻塞父进程
void sigcb(int no)
{
    while(waitpid(-1, NULL, WNOHANG) > 0);
}

int main(int argc, char *argv[])
{
	if (argc != 3)
	{
		std::cout << "Usage:./tcp_srv ip port" << std::endl;
		return -1;
	}
    //子进程退出处理方式
	signal(SIGCHLD, sigcb);

	std::string ip = argv[1];
	uint16_t port = std::stoi(argv[2]);

	TcpSocket lst_sock;
	//创建套接字
	lst_sock.Socket();
	//为套接字绑定地址信息
	lst_sock.Bind(ip, port);
	//开始监听
	lst_sock.Listen();
	while (1)
	{
		TcpSocket new_sock;
		//获取连接
		bool ret = lst_sock.Accept(&new_sock);
		if (!ret)
		{
			continue;//服务端不能因为获取一个新建套接字失败就退出
		}
		//创建子进程
		int pid = fork();  // 成功返回一个非0正值，失败返回-1
		if (pid == 0) // 父进程不会进来，因为父进程中pid>0，子进程会进入判断，子进程中pid==0
		{
			while (1)
			{
				std::string buf;
				new_sock.Recv(&buf);
				std::cout << "client say: " << buf << std::endl;

				buf.clear();
				std::cout << "server say: ";
				std::cin >> buf;
				new_sock.Send(buf);
			}
			new_sock.Close();
			exit(0);
		}
		new_sock.Close();//父进程关闭自己不使用的socket，不影响子进程
	}
	lst_sock.Close();
	return 0;
}
*/

/* 
多线程版本
	CMakeLists.txt中添加
			find_package(Threads)
			target_link_libraries(server-demo ${CMAKE_THREAD_LIBS_INIT})
	线程如何拿到新建的套接字对象呢？new_sock是一个局部对象，循环完毕的时候就会被释放，传地址就会造成错误。
	两种方案：1、每次accept的时候new_sock在堆上申请一个新的，不会被自动释放，但是也要防止第二次获取的时候覆盖上次的值；
			2、或者直接传值(将获取连接的套接字传到线程入口函数中)–在函数栈中会新建空间存储值，仅限于数据占用空间比较小，防止栈溢出

*/

void* thread_start(void *arg)
{
	long fd = (long)arg;
	TcpSocket new_socket;
	new_socket.SetFd(fd);
	while (true)
	{
		std::string outputBuf, inputBuf;
		new_socket.Recv(&outputBuf, 1);
		std::cin >> inputBuf;
		if(outputBuf != "")
		{
			std::cout << "client say:" << outputBuf << std::endl;
			outputBuf.clear();
		}
		if(inputBuf != "")
		{
			std::cout << "server say:" << inputBuf;
			new_socket.Send(inputBuf);
			inputBuf.clear();
		}
		std::cout << "outputBuf=" << outputBuf << std::endl;
		std::cout << "inputBuf=" << inputBuf << std::endl;
	}
	new_socket.Close();  // 线程之间文件描述符表共享，此处关闭其余地方也无法使用
	return nullptr;
}

int main(int argc, char *argv[])
{
	if(argc != 3)
	{
		std::cout << "Usage:./server-demo ip port" << std::endl;
		return -1;
	}
	std::string ip = argv[1];
	uint16_t port = std::stoi(argv[2]);

	TcpSocket server_sock;
	// 创建套接字
	server_sock.Socket();

	// 为套接字绑定地址信息
	server_sock.Bind(ip, port);

	// 开始监听
	server_sock.Listen();

	while (true)
	{
		TcpSocket new_socket;

		//获取连接
		bool ret = server_sock.Accept(&new_socket);
		if(!ret)
		{
			continue;   // 服务端不能因为获取一个新建套接字失败就退出
		}
		//线程
		pthread_t threadId;
		int res = pthread_create(&threadId, nullptr, thread_start, (void*)new_socket.GetFd());  
		/*
			pthread_create为创建线程函数，thread_start参数必须为void* thread_start(void *arg)类型的函数声明， 

			 (1）threadId：事先创建好的pthread_t类型的参数。成功时tidp指向的内存单元被设置为新创建线程的线程ID。
			（2）nullptr：用于定制各种不同的线程属性。APUE的12.3节讨论了线程属性。通常直接设为NULL/nullptr
			（3）thread_start：新创建线程从此函数开始运行。无参数是arg设为NULL即可。
			（4）(void*)new_socket.GetFd()：thread_start函数的参数。无参数时设为NULL即可。有参数时输入参数的地址。当多于一个参数时应当使用结构体传入
			5、返回值：成功返回0，否则返回错误码。
		*/
		if(res != 0)
		{
			std::cout << "pthread create error" << std::endl;
			continue;
		}
		pthread_detach(threadId);  // 不关心线程返回指，也不想等待释放资源，故将线程分离出去
		/*
			默认情况下，当线程终止时，其它线程可以通过调用 pthread_join()获取其返回状态、回收线程资源，
			有时，程序员并不关心线程的返回状态，只是希望系统在线程终止时能够自动回收线程资源并将其移除。
			在这种情况下，可以调用 pthread_detach()将指定线程进行分离，即分离线程

			一旦线程处于分离状态，就不能再使用 pthread_join()来获取其终止状态，此过程是不可逆的，
			一旦处于分离状态之后便不能再恢复到之前的状态。处于分离状态的线程，当其终止后，能够自动回收线程资源
		*/
	}
	server_sock.Close();
	return 0;
	
}











